package com.puzzlebazar.server;

import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Vector;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;


import com.puzzlebazar.client.main.model.PuzzleListInfoImpl.ListType;
import com.puzzlebazar.client.puzzle.model.History;
import com.puzzlebazar.client.puzzle.model.HistoryImpl;
import com.puzzlebazar.client.puzzle.model.MoveImpl;
import com.puzzlebazar.client.puzzle.model.PuzzleModel;
import com.puzzlebazar.client.puzzleinfo.HistoryInfo;
import com.puzzlebazar.client.puzzleinfo.NewPuzzleInfo;
import com.puzzlebazar.client.puzzleinfo.PuzzleAndHistoryInfo;
import com.puzzlebazar.client.puzzleinfo.PuzzleInfo;
import com.puzzlebazar.client.puzzleinfo.PuzzleInfo.Status;
import com.puzzlebazar.client.services.PuzzleService;
import com.puzzlebazar.client.user.UserInfo;
import com.puzzlebazar.client.util.Capabilities;

public class PuzzleServiceImpl extends MainServiceServlet implements
PuzzleService {

	private static final long serialVersionUID = 7034877868398073394L;

	@SuppressWarnings("unchecked")
	@Override
	public LinkedList<HistoryInfo> getHistoryList(String authorEmail, String puzzleInfoKey) {
		final UserInfo userInfo = getCurrentUserInfo();
	
		try {
			if( authorEmail != null && !Capabilities.canRequestHistoryList(userInfo, authorEmail) )
				throw new Exception( "Illegal request for history list" );
	
		} catch( Exception exception ) {
			exception.printStackTrace();
			return new LinkedList<HistoryInfo>();
		}

		final PersistenceManager pm = PMF.get().getPersistenceManager();
	    final Query query = pm.newQuery( HistoryInfoJdo.class );	

		query.setOrdering( "dateCompleted DESC, dateEdited DESC" );
	    
		if( authorEmail == null ) {
			// All completed puzzles
			query.setFilter( "dateCompleted > null && puzzleInfoKey == \"" + puzzleInfoKey + "\"" );
		}
		else {
			query.setFilter( "authorEmail == \"" + authorEmail + "\" && puzzleInfoKey == \"" + puzzleInfoKey + "\"" );
		}
		
		LinkedList<HistoryInfo> result = new LinkedList<HistoryInfo>();
		List<HistoryInfoJdo> queryResult;
		queryResult = (List<HistoryInfoJdo>)query.execute();

		ListIterator<HistoryInfoJdo> sourceIterator = queryResult.listIterator();
		
		while( sourceIterator.hasNext() ) {
			HistoryInfo element = new HistoryInfo();
			sourceIterator.next().to(element, false);
			result.add( element );
		}
		
		return result;
	}
		
	@Override
	public LinkedList<PuzzleInfo> getPuzzleList(ListType listType, Vector<String> genreList) {
		final UserInfo userInfo = getCurrentUserInfo();

	    PuzzleListFilter filter;
	    
		// See setCapabilities() to design SQL command
		try {
			switch( listType ){
				case ALL_PUZZLES:
					filter = CapabilitiesServer.canLoadPuzzleFilter( userInfo );
				break;
				case PLAYABLE_PUZZLES:
					filter = CapabilitiesServer.canPlayPuzzleFilter( userInfo );
				break;
				case EDITABLE_PUZZLES:
					if( userInfo == null ) 
						return new LinkedList<PuzzleInfo>();
					filter = CapabilitiesServer.canEditPuzzleFilter( userInfo );
				break;
				case VIEWABLE_TUTORIALS:
					filter = CapabilitiesServer.canViewTutorialFilter( userInfo );
				break;
				case EDITABLE_TUTORIALS:
					if( userInfo == null ) 
						return new LinkedList<PuzzleInfo>();
					filter = CapabilitiesServer.canEditTutorialFilter( userInfo );
				break;
				default:
					throw new Exception( "Unknown puzzle list type" );
		}
		} catch( Exception exception ) {
			exception.printStackTrace();
			return new LinkedList<PuzzleInfo>();
		}
	    filter.setGenres( genreList );
	    
	    LinkedList<PuzzleInfo> result = filter.runQuery();
	
	    if( userInfo != null ) {
	    	for (PuzzleInfo puzzleInfo : result) {
		    	getUserSpecificPuzzleInfo( puzzleInfo, userInfo );				
			}
	    }
	    
	    return result;
	}
	

	@Override
	public String createNewPuzzleOrTutorial(NewPuzzleInfo newPuzzleInfo) {
		
		final UserInfo userInfo = getCurrentUserInfo();
		if( userInfo == null )
			return null;
		
    	if( newPuzzleInfo.getAuthorEmail() == null )
    		newPuzzleInfo.setAuthorEmail( userInfo.getEmail() );
		
    	if( newPuzzleInfo.getTitle() == null || newPuzzleInfo.getTitle().length() == 0 )
    		newPuzzleInfo.setTitle( "Untitled" );
    	
    	if( !Capabilities.canCreatePuzzleOrTutorial(userInfo, newPuzzleInfo ) )
    		return null;
    	
		PuzzleInfoJdo jdo = new PuzzleInfoJdo();
	    final PersistenceManager pm = PMF.get().getPersistenceManager();
	

		try {
			jdo.create( newPuzzleInfo );
			pm.makePersistent( jdo );
			if( newPuzzleInfo.isTutorial() ) {
				String historyInfoKey = createNewHistoryInfo( new HistoryImpl(), jdo.getKey(), false );
				jdo.setTutorialHistoryKey( historyInfoKey );
			}
		
		} catch (Exception exception) {
			exception.printStackTrace();
			return null;
		}
		finally {
			pm.close();
		}

		
		return jdo.getKey();
	}



	@Override
	public boolean renamePuzzleOrTutorial(String puzzleKey, String title) {
		
	    final PersistenceManager pm = PMF.get().getPersistenceManager();
	    try {
	        PuzzleInfoJdo jdo = pm.getObjectById(PuzzleInfoJdo.class, puzzleKey);
		    PuzzleInfo puzzleInfo = new PuzzleInfo();
	    	jdo.to( puzzleInfo, false );	        
	        if( !Capabilities.canRenamePuzzleOrTutorial(getCurrentUserInfo(), puzzleInfo ) )
	        	return false;
	        jdo.setTitle(title);
	    } catch( Exception exception ) {
			exception.printStackTrace();
	    	return false;
	    } finally {
	        pm.close();
	    }
	
		return true;
	}

	@Override
	public boolean changePuzzleOrTutorialStatus(String puzzleKey, Status status) {

	    final PersistenceManager pm = PMF.get().getPersistenceManager();
	    try {
	        PuzzleInfoJdo jdo = pm.getObjectById(PuzzleInfoJdo.class, puzzleKey);
		    PuzzleInfo puzzleInfo = new PuzzleInfo();
	    	jdo.to( puzzleInfo, false );	        
	        if( !Capabilities.canChangePuzzleOrTutorialStatus(getCurrentUserInfo(), status, puzzleInfo ) )
	        	return false;
	        jdo.setStatus(status.toString());
	        if( status == Status.PUBLISHED ||
	        		status == Status.REVIEW_PUBLISHED )
	        	jdo.setDatePublished( new Date() );
	        if( status == Status.PRIVATE )
	        	jdo.setDatePublished(null);
	    } catch( Exception exception ) {
			exception.printStackTrace();
	    	return false;
	    } finally {
	        pm.close();
	    }
	
		return true;
	}
	
	@Override
	public boolean deletePuzzleOrTutorial(String puzzleKey) {

	    final PersistenceManager pm = PMF.get().getPersistenceManager();
	    try {
	        PuzzleInfoJdo jdo = pm.getObjectById(PuzzleInfoJdo.class, puzzleKey);
		    PuzzleInfo puzzleInfo = new PuzzleInfo();
	    	jdo.to( puzzleInfo, false );	        
	        if( !Capabilities.canDeletePuzzleOrTutorial(getCurrentUserInfo(), puzzleInfo ) )
	        	return false;
	        // TODO Delete attached history if this is a tutorial
	        pm.deletePersistent( jdo );
	    } catch( Exception exception ) {
			exception.printStackTrace();
	    	return false;
	    } finally {
	        pm.close();
	    }
	
		return true;
	}


	@Override
	public boolean savePuzzleModel(PuzzleModel model) {
	    final PersistenceManager pm = PMF.get().getPersistenceManager();
	    try {
	        PuzzleInfoJdo jdo = pm.getObjectById(PuzzleInfoJdo.class, model.getPuzzleInfoKey());
		    PuzzleInfo puzzleInfo = new PuzzleInfo();
	    	jdo.to( puzzleInfo, false );	        
	        if( !Capabilities.canEditPuzzle(getCurrentUserInfo(), puzzleInfo ) )
	        	return false;
	        jdo.setDateEdited( new Date() );
	        jdo.attachPuzzleModel(model);
	    } catch( Exception exception ) {
			exception.printStackTrace();
	    	return false;
	    } finally {
	        pm.close();
	    }
	
		return true;
	}


	@Override
	public String saveHistory(History model, String puzzleInfoKey, boolean completed) {
		
		if( model.getHistoryInfoKey() == null )
			return createNewHistoryInfo( model, puzzleInfoKey, completed );
		
		final PersistenceManager pm = PMF.get().getPersistenceManager();
	    try {
	    	HistoryInfoJdo jdo = pm.getObjectById(HistoryInfoJdo.class, model.getHistoryInfoKey());
		    HistoryInfo historyInfo = new HistoryInfo();
	    	jdo.to( historyInfo, false );
	    	assert( historyInfo.getPuzzleInfoKey().equals( puzzleInfoKey ) );
	        if( !Capabilities.canEditHistory(getCurrentUserInfo(), historyInfo ) )
	        	return null;
	        jdo.setDateEdited( new Date() );
	        if( completed )
	        	jdo.setDateCompleted( new Date() );
	        jdo.attachHistoryModel(model);
	    } catch( Exception exception ) {
			exception.printStackTrace();
	    	return null;
	    } finally {
	        pm.close();
	    }
	
		return model.getHistoryInfoKey();
	}
	
	@Override
	public String saveTutorial(History historyModel, PuzzleModel puzzleModel) {

		String puzzleInfoKey = puzzleModel.getPuzzleInfoKey();
		assert( puzzleInfoKey != null );		
		
	    final PersistenceManager pm = PMF.get().getPersistenceManager();
	    try {
	        PuzzleInfoJdo jdo = pm.getObjectById(PuzzleInfoJdo.class, puzzleInfoKey);
		    PuzzleInfo puzzleInfo = new PuzzleInfo();
	    	jdo.to( puzzleInfo, false );
	        if( !Capabilities.canEditTutorial(getCurrentUserInfo(), puzzleInfo ) )
	        	return null;
	        // Clear the model, all the information is contained in the history
	        puzzleModel.clear( MoveImpl.NO_TIME );
	        jdo.setDateEdited( new Date() );
	        jdo.attachPuzzleModel( puzzleModel );
	        String historyInfoKey = puzzleInfo.getTutorialHistoryKey();
	        if( historyInfoKey == null )
	        	throw new Exception( "Tutorial without an history!" );
	        String currentHistoryInfoKey = historyModel.getHistoryInfoKey();
	        if( currentHistoryInfoKey != null && !currentHistoryInfoKey.equals(historyInfoKey) )
	        	throw new Exception( "Tutorial with a new history!" );
        	jdo.setTutorialHistoryKey( historyInfoKey );
        	historyModel.setHistoryInfoKey( historyInfoKey );
        	saveHistory( historyModel, puzzleInfoKey, false );
	    } 
	    catch( Exception exception ) {
			exception.printStackTrace();
	    	return null;
	    } 
	    finally {
	    	pm.close();
	    }
			
		return puzzleInfoKey;
	}

	@Override
	public PuzzleInfo loadPuzzle(String puzzleInfoKey) {
		
	    PuzzleInfo puzzleInfo = loadPuzzleForced(puzzleInfoKey);
	    
		final UserInfo userInfo = getCurrentUserInfo();
		if( !Capabilities.canLoadPuzzle(userInfo, puzzleInfo ) )
			return null;
		
		getUserSpecificPuzzleInfo( puzzleInfo, userInfo );
			
	    return puzzleInfo;
	}

	@Override
	public PuzzleAndHistoryInfo loadHistory( String historyInfoKey  ) {

		HistoryInfo historyInfo = loadHistoryForced(historyInfoKey);
		String puzzleInfoKey = historyInfo.getPuzzleInfoKey();
		PuzzleInfo puzzleInfo = loadPuzzleForced(puzzleInfoKey);
	    
    	if( !Capabilities.canLoadHistory(getCurrentUserInfo(), puzzleInfo, historyInfo) )
    		return null;

    	return new PuzzleAndHistoryInfo( puzzleInfo, historyInfo);
	}

	@Override
	public PuzzleAndHistoryInfo loadTutorial(String puzzleInfoKey) {

		PuzzleInfo puzzleInfo = loadPuzzleForced(puzzleInfoKey);
		String historyInfoKey = puzzleInfo.getTutorialHistoryKey();
		HistoryInfo historyInfo = loadHistoryForced(historyInfoKey);
	    
    	if( !Capabilities.canLoadTutorial(getCurrentUserInfo(), puzzleInfo, historyInfo) )
    		return null;

    	return new PuzzleAndHistoryInfo( puzzleInfo, historyInfo);
	}

	/**
	 * A method that loads a puzzle without checking user capabilities
	 * 
	 * @param puzzleInfoKey The key to the puzzle to load
	 * @return The puzzle info, with the model filled in
	 */
	private PuzzleInfo loadPuzzleForced(String puzzleInfoKey) {
		final PersistenceManager pm = PMF.get().getPersistenceManager();
	    PuzzleInfo puzzleInfo = null;
	    PuzzleInfoJdo jdo = null;
	    try {
	    	jdo = pm.getObjectById(PuzzleInfoJdo.class, puzzleInfoKey);
		    puzzleInfo = new PuzzleInfo();
	    	jdo.to( puzzleInfo, true );
	    } catch( Exception exception ) {
			exception.printStackTrace();
			return null;
	    } finally {
	        pm.close();
	    }
		return puzzleInfo;
	}

	/**
	 * A method that loads a history without checking user capabilities
	 * 
	 * @param historyInfoKey The key to the history to load
	 * @return The history info, with the models filled in
	 */
	private HistoryInfo loadHistoryForced( String historyInfoKey  ) {
		final PersistenceManager pm = PMF.get().getPersistenceManager();
	    HistoryInfo historyInfo = null;
	    HistoryInfoJdo historyJdo = null;
	    try{
	    	historyJdo = pm.getObjectById(HistoryInfoJdo.class, historyInfoKey );
	    	historyInfo = new HistoryInfo();
	    	historyJdo.to( historyInfo, true );
	    }
	    catch( Exception exception ) {
			exception.printStackTrace();
	    	return null;
	    } 
	    finally {
	        pm.close();
	    }

	    return historyInfo;
	
	}
	
	/**
	 * Creates and save a new history info structure for the specified history model
	 * 
	 * @param model The model for which to create an history info
	 * @param puzzleInfoKey The model for which to create an history info
	 * @param completed True if the history is for a correctly completed puzzle, false otherwise
	 * @return the newly created history info key if successfull, null if failed
	 */
	private String createNewHistoryInfo(History model, String puzzleInfoKey, boolean completed) {
		
		final UserInfo userInfo = getCurrentUserInfo();
		if( userInfo == null )
			return null;
		
		HistoryInfo newHistoryInfo = new HistoryInfo();
		newHistoryInfo.setAuthorEmail( userInfo.getEmail() );
		newHistoryInfo.setAuthorUsername( userInfo.getNickname() );
		newHistoryInfo.setPuzzleInfoKey(puzzleInfoKey);
    	
    	if( !Capabilities.canCreateHistory(userInfo, newHistoryInfo ) )
    		return null;
    	
		HistoryInfoJdo jdo = new HistoryInfoJdo();
	    final PersistenceManager pm = PMF.get().getPersistenceManager();

		try {
			jdo.create( newHistoryInfo );
			pm.makePersistent( jdo );
			newHistoryInfo.setKey( jdo.getKey() );
			Date date = new Date();
			model.setHistoryInfoKey( jdo.getKey() );
			jdo.setDateEdited( date );
			if( completed )
				jdo.setDateCompleted( date );
			jdo.attachHistoryModel( model );
		} catch (Exception e) {
			e.printStackTrace();
			pm.deletePersistent( jdo );
			return null;
		}
		finally {
			pm.close();
		}
	
		return jdo.getKey();
	}


	/**
	 * Fill all the user-specific fields of the PuzzleInfo structure
	 * 
	 * TODO Use MemCache instead of systematically calling the datastore
	 * 
	 * @param puzzleInfo The structure to populate (the fields other than the 
	 * 					 user-specific fields should already be populated)
	 * @param userInfo The currently logged-in user
	 */
	private void getUserSpecificPuzzleInfo(PuzzleInfo puzzleInfo, UserInfo userInfo) {
		if( userInfo == null )
			return;
		
		HistoryInfoJdo result;
		
		puzzleInfo.setSpecificUserEmail( userInfo.getEmail() );
		
		final PersistenceManager pm = PMF.get().getPersistenceManager();

		final Query querySolved = pm.newQuery( HistoryInfoJdo.class );	
	    querySolved.setUnique(true);
	    querySolved.setRange(0,1);
	    querySolved.setFilter( "dateCompleted > null && authorEmail == \"" + userInfo.getEmail() + 
				"\" && puzzleInfoKey == \"" + puzzleInfo.getKey() + "\"" );
	    querySolved.setOrdering( "dateCompleted DESC, dateEdited DESC" );
	    result = (HistoryInfoJdo) querySolved.execute();
	    
	    if( result == null )
	    	puzzleInfo.setSolvedByUserHistoryKey( null );
	    else
	    	puzzleInfo.setSolvedByUserHistoryKey( result.getKey() );
	    
	    
	    final Query queryStarted = pm.newQuery( HistoryInfoJdo.class );	
	    queryStarted.setUnique(true);
	    queryStarted.setFilter( "dateCompleted == null && authorEmail == \"" + userInfo.getEmail() + 
				"\" && puzzleInfoKey == \"" + puzzleInfo.getKey() + "\"" );
	    queryStarted.setOrdering( "dateCompleted DESC, dateEdited DESC" );
	    queryStarted.setRange(0,1);
	    result = (HistoryInfoJdo) queryStarted.execute();

	    if( result == null )
	    	puzzleInfo.setStartedByUserHistoryKey( null );
	    else
	    	puzzleInfo.setStartedByUserHistoryKey( result.getKey() );
	}


}
